﻿using System;
using System.Collections;
using System.Linq;
using System.Reflection;
using System.Xml;

namespace Renci.Data.Interop.Serialization
{

    internal class PropertySerializationInfo
    {
        public PropertySerializationInfo(PropertyInfo property)
        {
            this.Property = property;

            //  Set specified 
            if (property.PropertyType.IsValueType)
            {
                this.SpecifiedProperty = property.DeclaringType.GetProperty(string.Format("{0}Specified", property.Name));
            }

            //this.Attributes = new Collection<PropertyAttributeSerializationInfo>();
        }

        //public ICollection<PropertyAttributeSerializationInfo> Attributes { get; protected set; }

        public PropertyInfo Property { get; protected set; }

        public PropertyInfo SpecifiedProperty { get; protected set; }

        public PropertyInfo ChoiceField { get; set; }

        public object DefaultValue { get; set; }

        public bool HasElements { get; set; }

        public bool HasAttributes { get; set; }

        public bool HasDefaultValue(object value)
        {
            if (value == null || this.DefaultValue == null)
            {
                return false;
            }

            return value.Equals(this.DefaultValue);
        }

        public bool IsText { get; set; }

        public bool IsIgnore { get; set; }

        public PropertyAttributeSerializationInfo ArrayRootAttribute { get; set; }

        public object GetPropertyValue(object obj)
        {
            var propertyValue = this.Property.GetValue(obj, null);

            if (this.SpecifiedProperty != null && this.SpecifiedProperty.PropertyType.IsValueType)
            {
                var isSpecified = (bool)this.SpecifiedProperty.GetValue(obj, null);

                if (!isSpecified)
                {
                    return null;
                }
            }

            return propertyValue;
        }

        public XmlQualifiedName GetChoicePropertyValue(object obj)
        {
            if (this.ChoiceField == null)
                return null;

            //  Get value from the property to be matched against attribute values
            //var value = GetPropertyValue(obj, this.ChoiceField, null);
            var value = this.ChoiceField.GetValue(obj, null);

            if (value is Enum)
            {
                var field = value.GetType().GetField(value.ToString());

                var name = field.Name;
                var ns = string.Empty;

                var enumAttribute = field.GetCustomAttributes(true).OfType<System.Xml.Serialization.XmlEnumAttribute>().SingleOrDefault();
                if (enumAttribute != null)
                {
                    name = enumAttribute.Name;
                    ns = string.Empty;

                    var i = enumAttribute.Name.LastIndexOf(':');
                    if (i > 0)
                    {
                        //  If enumvalue include namespace that extract it and create correct XmlQualifiedName
                        name = enumAttribute.Name.Substring(i + 1);
                        ns = enumAttribute.Name.Substring(0, i);
                    }
                }
                return new XmlQualifiedName(name, ns);
            }
            else
            {
                throw new NotImplementedException("Not enum for XmlChoice not supported.");
            }            
        }

        protected void SetChoicePropertyValue(object obj, XmlQualifiedName qn)
        {
            if (this.ChoiceField == null)
                return;

            if (this.ChoiceField.PropertyType == typeof(string))
            {
                this.ChoiceField.SetValue(obj, qn.ToString(), null);
            }
            else if (this.ChoiceField.PropertyType.IsEnum)
            {
                var propertyValue = GetEnumValue(this.ChoiceField, qn.ToString());
                this.ChoiceField.SetValue(obj, propertyValue, null);
            }
        }

        public virtual void SetPropertyValue(object obj, object value)
        {
            if (SetPropertySimpleValue(obj, value))
            {
                return;
            }
            else if (SetPropertyEnumerableValue(obj, value))
            {
                return;
            }
            else
            {
                throw new InvalidOperationException(string.Format("Cannot set property {0}.{1} to value {2}", this.Property.DeclaringType.FullName, this.Property.Name, value));
            }
        }

        private bool SetPropertySimpleValue(object obj, object value)
        {
            PropertyInfo property = this.Property;

            var propertyValue = value;

            if (property.PropertyType.IsInstanceOfType(propertyValue))
            {
                if (property.CanWrite)
                {
                    //  If property type is the same as object type then just make sure its not value type
                    //  and set Specified property to true if required
                    property.SetValue(obj, propertyValue, null);
                }
                else
                {
                    throw new InvalidOperationException(string.Format("Property '{0}.{1}' is read only.", property.DeclaringType.FullName, property.Name));
                }

                if (property.PropertyType.IsValueType && propertyValue != null)
                {
                    if (this.SpecifiedProperty != null)
                    {
                        this.SpecifiedProperty.SetValue(obj, true, null);
                    }
                }

                return true;
            }
            else if (property.PropertyType.IsPrimitive && propertyValue is string)
            {
                //  If type is primitive and value is string then SetValue will know how to convert from string to correct property value type
                if (propertyValue is IConvertible)
                {
                    propertyValue = Convert.ChangeType(propertyValue, property.PropertyType);
                }

                return SetPropertySimpleValue(obj, propertyValue);
            }
            else if (property.PropertyType.IsEnum && propertyValue is string)
            {
                propertyValue = GetEnumValue(property, propertyValue as string);

                return SetPropertySimpleValue(obj, propertyValue);
            }

            return false;
        }

        private bool SetPropertyEnumerableValue(object obj, object value)
        {
            var property = this.Property;

            if (property.PropertyType.GetInterface("IEnumerable") == null)
            {
                return false;
            }

            if (property.PropertyType.IsArray)
            {
                var array = property.GetValue(obj, null) as Array;

                if (array == null)
                {
                    //  Create new array object and set property value to it
                    array = Array.CreateInstance(InteropSerializer.GetElementType(property.PropertyType), 1);
                    if (!SetPropertySimpleValue(obj, array))
                    {
                        throw new InvalidOperationException("???1");
                    }
                }
                else
                {
                    //  Create new array object with Length + 1 from previous array, copy previous data from old one to new one
                    var newarray = Array.CreateInstance(InteropSerializer.GetElementType(property.PropertyType), array.Length + 1);
                    array.CopyTo(newarray, 0);
                    array = newarray;

                    if (!SetPropertySimpleValue(obj, array))
                    {
                        throw new InvalidOperationException("???2");
                    }
                }

                array.SetValue(value, array.Length - 1);

                return true;
            }
            else if (property.PropertyType.GetInterface("IList") != null)
            {
                var propertyValue = property.GetValue(obj, null) as IList;

                if (propertyValue == null && property.CanWrite)
                {
                    propertyValue = Activator.CreateInstance(property.PropertyType) as IList;

                    //  Create new instance of the list
                    if (!SetPropertySimpleValue(obj, propertyValue))
                    {
                        throw new InvalidOperationException("???3");
                    }
                }

                propertyValue.Add(value);

                return true;
            }
            else if (property.PropertyType.GetInterface("ICollection") != null)
            {
                var newPropertyValue = property.GetValue(obj, null) as ICollection;

                if (newPropertyValue == null && property.CanWrite)
                {
                    newPropertyValue = Activator.CreateInstance(property.PropertyType) as ICollection;

                    //  Create new instance of the list
                    if (!SetPropertySimpleValue(obj, newPropertyValue))
                    {
                        throw new InvalidOperationException("???4");
                    }
                }

                MethodInfo addMethod = property.PropertyType.GetMethod("Add");
                if (addMethod == null)
                {
                    throw new InvalidOperationException(string.Format("Collection of type '{0}' doesnt have \"Add\" methods.", property.PropertyType.FullName));
                }

                addMethod.Invoke(newPropertyValue, new object[] { value });

                return true;
            }
            return false;
        }

        internal Enum GetEnumValue(PropertyInfo property, string value)
        {
            var type = InteropSerializer.GetElementType(property.PropertyType);
            if (!type.IsEnum)
            {
                throw new InvalidOperationException(string.Format("Property '{0}' not of enum type", property.Name));
            }

            var enumValue = value;

            var enumField = (from field in type.GetFields()
                             from attr in field.GetCustomAttributes(true).OfType<System.Xml.Serialization.XmlEnumAttribute>()
                             where attr.Name == value.ToString()
                             select field).SingleOrDefault();

            if (enumField != null)
            {
                enumValue = enumField.Name;
            }

            if (Enum.IsDefined(type, enumValue))
            {
                return (Enum)Enum.Parse(type, enumValue, true);
            }
            else
            {
                throw new InvalidOperationException(string.Format("Value {0} is not part of enum type {1}", enumValue, property.PropertyType.FullName));
            }
        }
    }
}
